IT Ops Portal
Operator & IT Manager Console
Dashboard
SLA Breach 30d
–
violazioni SLA
Problem aperti
–
da risolvere
Change pending
–
in approvazione
SR aperti
–
service request
🔴 Aperti –
🟡 In Attesa Cliente –
✅ Chiusi
📋 Tutti
Export:
⏳ Incident in status "In Attesa Cliente" — SLA sospeso. Il conteggio riprende quando il cliente risponde.
📅 Calendario Mensile
📋 Vista Settimanale
🕐 Orario Ordinario
📡 Reperibilità
📞 Log Chiamate
–
LUN
MAR
MER
GIO
VEN
SAB
DOM
L1
L2
L3
Manager
NOC H24
–
Lun–Ven08:00 – 18:00
SabatoChiuso
DomenicaChiuso
Fuso orarioEurope/Rome (CET/CEST)
Ora attuale–
Round-robin su operatori disponibili
Motivo chiamata
📞
Nessuna chiamata registrata
Clicca "Analizza" per correlare gli incident aperti e suggerire la causa radice.
CHANGE RISK ASSESSMENT
Inserisci la chiave di un change RFC e clicca "Valuta Rischio AI".
🧊 Change Freeze attivo — –
Ultimi CI aggiornati da discovery
📡 Webhook Endpoint — Discovery Esterna
–
Invia un POST a questo URL con il payload JSON. Compatibile con Zabbix Action, Nagios, Nmap NSE script, agenti custom.
Mostra payload di esempio
{
"project": "TENANT",
"source": "zabbix",
"cis": [
{
"hostname": "srv-app-01.azienda.it",
"ip": "10.0.1.10",
"type": "Server",
"os": "Ubuntu 24.04 LTS",
"environment": "Production",
"criticality": "High",
"services": "nginx, postgresql"
}
]
}
SLA BREACH PREDICTION
Analisi in corso...
COMPLIANCE ANALYSIS
Clicca per analizzare la postura di compliance.
Invia i findings del tuo scanner a questo endpoint. Compatibile con Tenable, Qualys, Nuclei, Trivy, Grype, Snyk e webhook personalizzati.
–
Mostra payload di esempio
{
"project": "TENANT",
"source": "nuclei",
"findings": [
{
"cve_id": "CVE-2024-12345",
"cvss": 9.8,
"severity": "Critical",
"title": "Remote Code Execution in Apache HTTP Server",
"host": "web-01.azienda.it",
"component": "apache/httpd:2.4.51",
"exploit_available": true,
"remediation_days": 7
}
]
}
Clicca "Esegui AIOps Now" per avviare la correlazione automatica degli incident aperti.
🎯 Ticket Triage
Classificazione automatica dei nuovi ticket aperti
🔍 RCA Assist
Correlazione incident → causa radice comune
⏱ SLA Breach Predict
Ticket a rischio SLA nelle prossime 4 ore
🚨 Anomaly Detection
Spike e degradazioni anomale per servizio
📊 Capacity Forecast
Previsione volume ticket prossime 4 settimane
📋 Runbook Generator
Genera runbook da storico ticket simili
⭐ Sentiment CSAT
Analisi soddisfazione utenti da CSAT
🛡 Compliance Check
Stato compliance GDPR/ISO27001/NIS2
📈 Incident Trend
Trend e predizioni 24h
ARTICOLO GENERATO
Inserisci la chiave di un ticket risolto per generare un articolo KB.
Repeat Incidents
–
ricorrenti
TREND ANALYSIS
Clicca per analizzare i trend con AI.
Alert attivi
–
NOC-EVENT aperti
Servizi degradati
–
uptime < 99.9%
Panoramica
Alert Queue
Availability
Runbook
Shift Handover
Analisi Predittiva
📋
Seleziona uno scenario
Il runbook apparirà qui
🤖
Clicca "Genera con AI"
Claude analizzerà la situazione e produrrà un briefing operativo completo
Incident attivi
–
SOC-INCIDENT
Vuln Critical
–
non remediate
Exploit disponibili
–
CVE con PoC
Panoramica
Alert Queue
Vulnerabilità
MITRE ATT&CK
Compliance
Workload Team
Timeline
🔍
Inserisci la chiave di un ticket
Verrà mostrata la timeline completa con tutti gli eventi, commenti e correlazioni
Panoramica Team
Leaderboard
Achievement
Previsioni Avanzate
La Mia Performance
Clicca su un badge per i dettagli
Operatore: –
🏅
Clicca "Carica i miei KPI"
👥 Operatori
⏱ SLA & Orari
🔐 Gruppi Jira
⚙️ Worker Config
📋 Audit Log
Gruppo
🔍
Clicca "Test connessione" per avviare il check
async function loadBridges() {
const el = document.getElementById('bridge-list');
if (!el) return;
el.innerHTML = '';
try {
const r = await workerGet(`/api/itsm/active-bridges?project=${P()}`);
const bridges = r.bridges || [];
// KPI
const active = bridges.length;
const avgAge = active > 0 ? Math.round(bridges.reduce((s,b) => s + b.age_minutes, 0) / active) : 0;
const comms = bridges.filter(b => b.comms_sent === 'Yes').length;
const pir = bridges.filter(b => b.pir_date).length;
document.getElementById('bri-kpi-active').textContent = active;
document.getElementById('bri-kpi-age').textContent = avgAge || '–';
document.getElementById('bri-kpi-comms').textContent = comms;
document.getElementById('bri-kpi-pir').textContent = pir;
// Badge sidebar
const badge = document.getElementById('bridge-badge');
if (badge) { badge.style.display = active > 0 ? '' : 'none'; badge.textContent = active; }
if (bridges.length === 0) {
el.innerHTML = '✅
Nessun Major Incident attivo
Tutti i sistemi operativi
';
return;
}
el.innerHTML = bridges.map(b => {
const ageColor = b.age_minutes > 120 ? 'var(--red)' : b.age_minutes > 60 ? 'var(--orange)' : 'var(--green)';
const ageLabel = b.age_minutes >= 60
? `${Math.floor(b.age_minutes/60)}h ${b.age_minutes%60}m`
: `${b.age_minutes}min`;
return `
${b.key}
BRIDGE ATTIVO
⏱ ${ageLabel}
${b.summary}
Commander: ${b.commander} · Servizio: ${b.affected_service||'–'}
${b.status_update ? `
Ultimo aggiornamento: ${b.status_update}
` : ''}
Comms clienti: ${b.comms_sent}
${b.pir_date ? `PIR: ${new Date(b.pir_date).toLocaleDateString('it-IT')}` : ''}
Stato: ${b.status}
`;
}).join('');
} catch(e) { el.innerHTML = '⚠️
Errore caricamento bridge
'; }
}
async function openBridgeDetail(key) {
try {
const r = await workerGet(`/api/issue?key=${key}`);
const f = r.issue?.fields || {};
document.getElementById('modal-bridge-key').textContent = `🌉 ${key} — Major Incident Bridge`;
document.getElementById('modal-bridge-body').innerHTML = `
${f.priority?.name||'–'}
${f.status?.name||'–'}
${f.summary||'–'}
Commander:
${f.customfield_incident_commander?.displayName||f.assignee?.displayName||'–'}
Servizio impattato:
${f.customfield_affected_service||'–'}
Comms clienti inviate:
${f.customfield_customer_comms_sent?.value||'No'}
PIR pianificata:
${f.customfield_pir_date?new Date(f.customfield_pir_date).toLocaleDateString('it-IT'):'Da pianificare'}
RCA:
${f.customfield_rca||'In corso...'}
${f.customfield_bridge_status_update ? `
Ultimo status update
${f.customfield_bridge_status_update}
` : ''}
${f.customfield_bridge_participants ? `
Partecipanti bridge
${f.customfield_bridge_participants}
` : ''}
`;
openModal('modal-bridge');
} catch(e) { nhToast('Errore caricamento bridge', 'error'); }
}
function declareMajorIncident() {
nhToast('Apri un ticket IT-MAJ in Jira per dichiarare un Major Incident — il bridge verrà creato automaticamente.', 'warning');
window.open(`${ITSMOPS_CONFIG.jira_base_url}/jira/software/projects/${P()}/boards`, '_blank');
}
// ══════════════════════════════════════════════════════════
// ON-CALL SCHEDULE
// ══════════════════════════════════════════════════════════
async function loadOnCall() {
const nowEl = document.getElementById('oncall-now');
const calEl = document.getElementById('oncall-calendar-list');
if (!nowEl || !calEl) return;
try {
const today = new Date().toISOString().split('T')[0];
const [nowR, calR] = await Promise.all([
workerGet(`/api/itsm/oncall-schedule?project=${P()}&date=${today}`),
workerGet(`/api/itsm/oncall-calendar?project=${P()}&weeks=4`),
]);
// Turno attivo ora
const active = nowR.schedules || [];
if (active.length === 0) {
nowEl.innerHTML = 'Nessun turno configurato per oggi.
';
} else {
nowEl.innerHTML = active.map(s => `
${s.assignee.split(' ').map(w=>w[0]).slice(0,2).join('')}
${s.assignee}
${s.rotation} · Tier ${s.tier} · Escalation: ${s.escalation_policy}
${s.contact_phone ? `
📞 ${s.contact_phone}` : ''}
Turno attivo
`).join('
');
}
// Calendario
const calendar = calR.calendar || [];
if (calendar.length === 0) {
calEl.innerHTML = '📅
Nessun turno pianificato
Aggiungi turni per le prossime settimane
';
return;
}
const tierColor = { L1:'var(--blue)', L2:'var(--purple)', L3:'var(--orange)', Manager:'var(--red)' };
calEl.innerHTML = `
| Oggetto | Responsabile | Rotazione | Tier |
Inizio | Fine | Telefono |
${calendar.map(i => {
const f = i.fields;
const tier = f.customfield_oncall_tier?.value || 'L1';
const start = f.customfield_oncall_schedule_date ? new Date(f.customfield_oncall_schedule_date).toLocaleString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '–';
const end = f.customfield_oncall_end_date ? new Date(f.customfield_oncall_end_date).toLocaleString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '–';
return `
| ${f.summary||i.key} |
${f.assignee?.displayName||'Non assegnato'} |
${f.customfield_oncall_rotation?.value||'–'} |
${tier} |
${start} |
${end} |
${f.customfield_oncall_contact_phone||'–'} |
`;
}).join('')}
`;
} catch(e) {
if (nowEl) nowEl.innerHTML = 'Errore caricamento
';
if (calEl) calEl.innerHTML = '';
}
}
async function submitOnCallTurno() {
const summary = document.getElementById('oc-summary')?.value?.trim();
const start = document.getElementById('oc-start')?.value;
const end = document.getElementById('oc-end')?.value;
const rotation = document.getElementById('oc-rotation')?.value;
const tier = document.getElementById('oc-tier')?.value;
const phone = document.getElementById('oc-phone')?.value?.trim();
const escalation = document.getElementById('oc-escalation')?.value;
if (!summary || !start || !end) { nhToast('Compila oggetto, inizio e fine turno', 'warning'); return; }
try {
const fields = {
project: { key: P() },
issuetype: { name: `${P()}-IT-ONCALL` },
summary,
customfield_oncall_schedule_date: start,
customfield_oncall_end_date: end,
customfield_oncall_rotation: rotation ? { value: rotation } : undefined,
customfield_oncall_tier: tier ? { value: tier } : undefined,
customfield_oncall_contact_phone: phone || undefined,
customfield_oncall_escalation_policy: escalation ? { value: escalation } : undefined,
};
// Rimuovi campi undefined
Object.keys(fields).forEach(k => fields[k] === undefined && delete fields[k]);
const r = await workerPost('/api/issue', { body: { fields } });
if (r.key) {
nhToast(`Turno ${r.key} creato!`, 'success');
closeModal('modal-oncall');
loadOnCall();
}
} catch(e) { nhToast('Errore creazione turno: ' + (e.message||''), 'error'); }
}
function openNewOnCallModal() {
// Pre-compila data/ora di default (ora corrente + 8 ore)
const now = new Date();
const end = new Date(now.getTime() + 8 * 3600000);
const fmt = d => d.toISOString().slice(0,16);
const startEl = document.getElementById('oc-start');
const endEl = document.getElementById('oc-end');
if (startEl) startEl.value = fmt(now);
if (endEl) endEl.value = fmt(end);
openModal('modal-oncall');
}
// ══ HOOK: Aggiorna showPage per Bridge e OnCall ════════════
const _showPageOrig2 = typeof showPage === 'function' ? showPage : null;
if (_showPageOrig2) {
const _prevShowPage = window.showPage || _showPageOrig2;
window.showPage = function(name) {
_prevShowPage(name);
if (name === 'bridges') loadBridges();
if (name === 'oncall') loadOnCall();
if (name === 'sla') loadSLAFull();
if (name === 'pipeline') loadPipeline();
const titles2 = { bridges: 'Major Incident Bridge', oncall: 'On-Call Schedule', pipeline: 'Release Pipeline' };
if (titles2[name]) document.getElementById('topbar-title').textContent = titles2[name];
};
}
// ══ RELEASE PIPELINE ═══════════════════════════════════════════════════════
const PIPELINE_ENV_META = {
"Dev": { color: "#6366f1", bg: "#eef2ff", label: "DEV" },
"Test": { color: "#0891b2", bg: "#ecfeff", label: "TEST" },
"Staging": { color: "#d97706", bg: "#fffbeb", label: "STAGING" },
"Pre-Production": { color: "#7c3aed", bg: "#f5f3ff", label: "PRE-PROD" },
"Production": { color: "#059669", bg: "#ecfdf5", label: "PROD" },
};
const DEPLOY_STATUS_STYLE = {
"Success": { icon: "✅", badge: "badge-green" },
"Failed": { icon: "❌", badge: "badge-red" },
"Rolled Back":{ icon: "↩", badge: "badge-orange" },
"In Progress":{ icon: "⏳", badge: "badge-blue" },
"Cancelled": { icon: "⛔", badge: "badge-gray" },
};
async function loadPipeline() {
const days = document.getElementById('pipeline-days')?.value || '30';
const board = document.getElementById('pipeline-board');
if (board) board.innerHTML = '';
try {
const r = await workerGet(`/api/itsm/release-pipeline?project=${P()}&days=${days}`);
// Aggiorna KPI globali
const s = r.summary || {};
document.getElementById('pip-total').textContent = s.total ?? '–';
document.getElementById('pip-rate').textContent = s.success_rate != null ? s.success_rate : '–';
document.getElementById('pip-failed').textContent = s.failed ?? '–';
document.getElementById('pip-rollback').textContent = s.rollbacks ?? '–';
// Colora KPI rate
const rateEl = document.getElementById('pip-rate');
if (s.success_rate != null) {
rateEl.className = 'kpi-value ' + (s.success_rate >= 90 ? 'green' : s.success_rate >= 70 ? 'orange' : 'red');
}
// Render board
if (board) board.innerHTML = (r.columns || []).map(col => renderPipelineColumn(col)).join('');
} catch(e) {
if (board) board.innerHTML = '⚠️
Errore caricamento pipeline
';
}
}
function renderPipelineColumn(col) {
const meta = PIPELINE_ENV_META[col.environment] || { color: "#64748b", bg: "#f8fafc", label: col.environment };
const rateColor = col.success_rate == null ? '#64748b'
: col.success_rate >= 90 ? '#059669'
: col.success_rate >= 70 ? '#d97706' : '#dc2626';
const headerKpis = col.total > 0 ? `
✅ ${col.success}
${col.failed ? `❌ ${col.failed}` : ''}
${col.rollbacks ? `↩ ${col.rollbacks}` : ''}
${col.in_progress ? `⏳ ${col.in_progress}` : ''}
` : '';
const deployCards = col.deploys.length === 0
? 'Nessun deploy
'
: col.deploys.slice(0, 8).map(d => {
const ds = DEPLOY_STATUS_STYLE[d.deploy_status] || { icon: "•", badge: "badge-gray" };
const sha = d.commit_sha ? d.commit_sha.substring(0, 7) : '';
const dur = d.duration_min != null ? `${d.duration_min}m` : '';
const dateStr = d.created ? new Date(d.created).toLocaleDateString('it-IT', { day:'2-digit', month:'short' }) : '';
return `
${d.key}
${ds.icon}
${(d.build_id || d.summary).substring(0, 40)}${(d.build_id||d.summary).length > 40 ? '…' : ''}
${sha ? `${sha}` : ''}
${dur ? `⏱ ${dur}` : ''}
${dateStr ? `${dateStr}` : ''}
${d.rollback ? `↩ Rollback` : ''}
${d.pipeline_url ? `
→ Pipeline` : ''}
`;
}).join('');
return `
${meta.label}
${col.success_rate != null ? col.success_rate + '%' : '–'}
${col.total} deploy${col.avg_duration_min ? ` · avg ${col.avg_duration_min}m` : ''}
${headerKpis}
${deployCards}
`;
}
// ══════════════════════════════════════════════════════════
// AIOPS — Correlation, Anomaly Detection, Capacity Forecast
// ══════════════════════════════════════════════════════════
async function runAIOpsCorrelation() {
const panel = document.getElementById('aiops-correlation-panel');
if (!panel) return;
panel.innerHTML = 'Analisi correlazione in corso…
';
document.getElementById('aiops-kpi-clusters').textContent = '…';
document.getElementById('aiops-kpi-highconf').textContent = '…';
document.getElementById('aiops-kpi-problems').textContent = '…';
try {
const r = await workerPost('/api/itsm/aiops-correlate', { project: P() });
// KPI
document.getElementById('aiops-kpi-clusters').textContent = r.total_clusters ?? 0;
document.getElementById('aiops-kpi-highconf').textContent = r.high_confidence_count ?? 0;
document.getElementById('aiops-kpi-problems').textContent = r.auto_created_problem ? '1 ✓' : '0';
const clusters = r.clusters || [];
if (!clusters.length) {
panel.innerHTML = '✅ Nessuna correlazione significativa rilevata — ' + (r.incidents_analyzed||0) + ' incident analizzati.
';
return;
}
const confColor = c => c >= 80 ? 'var(--red)' : c >= 60 ? 'var(--orange)' : 'var(--blue)';
const actionLabel = { create_problem:'🔴 Crea Problem', merge_tickets:'🔗 Mergia ticket', escalate:'⬆ Escalation', monitor:'👁 Monitora' };
panel.innerHTML = `
${r.incidents_analyzed||0} incident analizzati · ${clusters.length} cluster trovati · Timestamp: ${r.timestamp ? new Date(r.timestamp).toLocaleTimeString('it-IT') : '–'}
${r.auto_created_problem ? `✅ Problem record creato automaticamente: ${r.auto_created_problem}
` : ''}
${clusters.map((c, i) => `
Cluster ${i+1}
Score: ${c.correlation_score}%
${actionLabel[c.recommended_action]||c.recommended_action}
Confidenza: ${c.confidence}%
🔧 Servizio: ${c.common_service||'–'}
📁 Categoria: ${c.common_category||'–'}
Root cause probabile: ${c.probable_root_cause}
${(c.incident_keys||[]).map(k => `
${k}`).join('')}
`).join('')}`;
} catch(e) {
panel.innerHTML = `Errore AIOps: ${e.message}
`;
document.getElementById('aiops-kpi-clusters').textContent = '–';
}
}
async function runAIOpsAnomaly() {
const el = document.getElementById('ai-tools-text');
const title = document.getElementById('ai-tools-result-title');
if (title) title.textContent = '🚨 Anomaly Detection';
if (el) el.textContent = 'Analisi anomalie in corso…';
try {
const r = await workerGet(`/api/itsm/ai-anomaly?project=${P()}`);
document.getElementById('aiops-kpi-anomalies').textContent = r.total_anomalies ?? 0;
const anomalies = r.anomalies || [];
if (!anomalies.length) {
if (el) el.textContent = `✅ Nessuna anomalia rilevata nel periodo analizzato. Incident correnti: ${r.current_period}, baseline: ${r.baseline_period}.`;
return;
}
const sevColor = { critical:'var(--red)', high:'var(--orange)', medium:'var(--blue)', low:'var(--green)' };
if (el) el.innerHTML = `
${r.current_period} incident (7gg) vs ${r.baseline_period} baseline · Generato: ${r.generated_at ? new Date(r.generated_at).toLocaleString('it-IT') : '–'}
${r.immediate_action_required ? '⚠️ Azione immediata richiesta
' : ''}
${anomalies.map(a => `
${a.service}
${a.severity}
${a.type}
${a.description}
Baseline: ${a.baseline_count} → Corrente: ${a.current_count} (score: ${a.anomaly_score})
→ ${a.recommended_action}
`).join('')}`;
} catch(e) {
if (el) el.textContent = 'Errore anomaly detection: ' + e.message;
}
}
async function runAIOpsCapacity() {
const el = document.getElementById('ai-tools-text');
const title = document.getElementById('ai-tools-result-title');
if (title) title.textContent = '📊 Capacity Forecast';
if (el) el.textContent = 'Generazione forecast in corso…';
try {
const r = await workerGet(`/api/itsm/ai-capacity-forecast?project=${P()}`);
const forecast = r.forecast || [];
if (!forecast.length) {
if (el) el.textContent = r.error || 'Dati storici insufficienti per generare un forecast.';
return;
}
if (el) el.innerHTML = `
Forecast basato su ${r.historical_weeks} settimane storiche · Generato: ${r.generated_at ? new Date(r.generated_at).toLocaleString('it-IT') : '–'}
| Settimana | Incident previsti | SR previste | Confidenza |
${forecast.map(f => `
| ${f.week} |
${f.predicted_incidents} |
${f.predicted_sr} |
${f.confidence}% |
`).join('')}
${r.staff_recommendation ? `Raccomandazione staff: ${r.staff_recommendation}
` : ''}
${r.risk_weeks?.length ? `⚠️ Settimane a rischio: ${r.risk_weeks.join(', ')}
` : ''}
${r.key_drivers?.length ? `Driver principali: ${r.key_drivers.join(' · ')}
` : ''}`;
} catch(e) {
if (el) el.textContent = 'Errore capacity forecast: ' + e.message;
}
}
async function runSelfHeal() {
const key = document.getElementById('selfheal-key')?.value?.trim();
if (!key) { nhToast('Inserisci la chiave ticket', 'warning'); return; }
const el = document.getElementById('selfheal-result');
if (!el) return;
el.style.display = 'block';
el.innerHTML = 'Generazione runbook AI in corso…
';
try {
const r = await workerPost('/api/ai-analyze', {
type: 'self-heal', project: P(),
context: { ticket_key: key, message: `Genera runbook self-heal per ticket ${key}` }
});
const result = r.result ? (typeof r.result === 'string' ? JSON.parse(r.result) : r.result) : {};
const analysis = result.incident_analysis || {};
const runbook = result.runbook || {};
const steps = runbook.steps || [];
el.innerHTML = `
${runbook.title || 'Runbook AI — ' + key}
🎯 Root cause: ${analysis.root_cause_hypothesis||'–'}
⏱ ETA: ${runbook.estimated_resolution_min||'–'} min
🤖 Automazione: ${result.automation_coverage_pct||0}%
📊 Confidenza: ${analysis.confidence||0}%
${steps.map(s => `
${s.step}
${s.action}
${s.command_or_api ? `
${s.command_or_api}` : ''}
✓ ${s.verification}
${s.requires_approval ? '
Richiede approvazione' : ''}
${s.automated?'AUTO':'MANUALE'}
`).join('')}
${result.escalation_required ? `
⬆ Escalation richiesta: ${result.escalation_reason}
` : ''}
`;
} catch(e) {
el.innerHTML = `Errore: ${e.message}
`;
}
}
// ══ HOOK showPage per AI-tools ═══════════════════════════
const _showPageOrig5 = window.showPage;
if (_showPageOrig5) {
window.showPage = function(name) {
_showPageOrig5(name);
if (name === 'ai-tools') {
document.getElementById('topbar-title').textContent = 'AI Tools & AIOps';
}
};
}
// ══════════════════════════════════════════════════════════
// i18n Operator — lingua sincronizzata con Employee
// ══════════════════════════════════════════════════════════
(function initOpsLang() {
try {
const lang = localStorage.getItem('nh_lang') || ITSMOPS_CONFIG.language || 'it';
if (lang !== 'it') {
const langLabels = { en: 'EN', es: 'ES', pt: 'PT', it: 'IT' };
const topbarUser = document.getElementById('topbar-uname');
if (topbarUser) {
const pill = document.createElement('span');
pill.style.cssText = 'font-size:10px;padding:1px 6px;border-radius:20px;background:var(--surface-3);color:var(--text-3);margin-left:4px;cursor:pointer';
pill.textContent = langLabels[lang] || 'IT';
pill.title = 'Language: ' + lang.toUpperCase();
topbarUser.parentElement?.appendChild(pill);
}
}
} catch(e) {}
})();
// ══════════════════════════════════════════════════════════
// NOC FUNCTIONS
// ══════════════════════════════════════════════════════════
function showNOCTab(tab) {
document.querySelectorAll('#page-noc .noc-soc-tab').forEach((t,i) => {
const tabs = ['overview','alerts','availability','runbook','handover','predictive'];
t.classList.toggle('active', tabs[i] === tab);
});
document.querySelectorAll('#page-noc .noc-soc-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById(`noc-panel-${tab}`);
if (panel) panel.classList.add('active');
if (tab === 'alerts') loadNOCAlerts();
if (tab === 'availability') loadNOCAvailability();
if (tab === 'handover') loadNOCHandoverData();
if (tab === 'predictive') loadNOCPredictive();
}
async function loadNOCDashboard() {
const period = document.getElementById('noc-period')?.value || '72';
try {
const r = await workerGet(`/api/noc/dashboard?project=${P()}&period=${period}`);
document.getElementById('noc-k-events').textContent = r.events_open ?? '–';
document.getElementById('noc-k-incidents').textContent = r.incidents_open ?? '–';
document.getElementById('noc-k-esc').textContent = r.escalations_open ?? '–';
document.getElementById('noc-k-mttr').textContent = r.mttr_avg_h != null ? r.mttr_avg_h : '–';
const degraded = (r.availability||[]).filter(s => s.availability_pct < 99.9).length;
document.getElementById('noc-k-degraded').textContent = degraded;
// Badge sidebar
const badge = document.getElementById('noc-badge');
if (badge && r.events_open > 0) { badge.style.display='inline'; badge.textContent = r.events_open; }
// SLA banner
const banner = document.getElementById('noc-sla-banner');
const bannerText = document.getElementById('noc-sla-banner-text');
if (r.at_risk_sla?.length > 0 && banner && bannerText) {
banner.style.display = 'flex';
bannerText.textContent = `⚠ ${r.at_risk_sla.length} ticket NOC a rischio SLA — ${r.at_risk_sla[0]?.key} P${r.at_risk_sla[0]?.priority} al ${r.at_risk_sla[0]?.sla_pct}%`;
}
// Grafici
buildNOCTrendChart(period);
buildNOCSourceChart(r.alert_by_source || []);
// P1 list
const p1El = document.getElementById('noc-p1-list');
const p1Items = (r.p1_incidents||[]);
p1El.innerHTML = p1Items.length === 0
? ''
: p1Items.map(i => nocAlertItemHTML(i)).join('');
// SLA risk list
const riskEl = document.getElementById('noc-sla-risk-list');
const riskItems = (r.at_risk_sla||[]);
riskEl.innerHTML = riskItems.length === 0
? '✅
Nessun ticket a rischio SLA
'
: riskItems.map(i => `
${i.key}
${i.summary||'–'} ${i.hostname||''}
${i.priority}
${i.elapsed_min}/${i.target_min} min (${i.sla_pct}%)
`).join('');
} catch(e) { console.error('loadNOCDashboard', e); }
}
async function buildNOCTrendChart(period) {
if (charts['noc-trend']) { charts['noc-trend'].destroy(); delete charts['noc-trend']; }
const canvas = document.getElementById('chart-noc-trend');
if (!canvas) return;
const days = Math.min(parseInt(period)/24, 30);
try {
const r = await workerPost('/api/search', {
jql: `project="${P()}" AND issuetype in ("${P()}-NOC-EVENT","${P()}-NOC-INCIDENT") AND created>=-${Math.ceil(days)}d ORDER BY created ASC`,
fields: ['created','issuetype'], maxResults: 500
});
const issues = r.issues || [];
const labels = [], events = [], incidents = [];
for (let i = Math.ceil(days)-1; i >= 0; i--) {
const d = new Date(); d.setDate(d.getDate()-i);
const ds = d.toISOString().split('T')[0];
labels.push(d.toLocaleDateString('it-IT',{day:'2-digit',month:'short'}));
events.push(issues.filter(iss => iss.fields.created?.startsWith(ds) && iss.fields.issuetype?.name?.includes('EVENT')).length);
incidents.push(issues.filter(iss => iss.fields.created?.startsWith(ds) && iss.fields.issuetype?.name?.includes('INCIDENT')).length);
}
charts['noc-trend'] = new Chart(canvas.getContext('2d'), {
type: 'line',
data: { labels, datasets: [
{ label:'Alert', data:events, borderColor:'#f97316', backgroundColor:'rgba(249,115,22,.08)', tension:.4, pointRadius:2, fill:true },
{ label:'Incident', data:incidents, borderColor:'#ef4444', backgroundColor:'rgba(239,68,68,.06)', tension:.4, pointRadius:2, fill:true },
]},
options: { responsive:true, maintainAspectRatio:false, plugins:{legend:{position:'bottom',labels:{font:{size:11},boxWidth:10}}},
scales:{ x:{ticks:{font:{size:10}},grid:{display:false}}, y:{beginAtZero:true,ticks:{font:{size:10}},grid:{color:'rgba(128,128,128,.08)'}} }
}
});
} catch(e) {}
}
function buildNOCSourceChart(sourcesData) {
if (charts['noc-source']) { charts['noc-source'].destroy(); delete charts['noc-source']; }
const canvas = document.getElementById('chart-noc-source');
if (!canvas || !sourcesData.length) return;
charts['noc-source'] = new Chart(canvas.getContext('2d'), {
type: 'doughnut',
data: {
labels: sourcesData.map(s=>s.source),
datasets:[{ data:sourcesData.map(s=>s.count),
backgroundColor:['rgba(249,115,22,.8)','rgba(239,68,68,.8)','rgba(59,130,246,.8)','rgba(34,197,94,.8)','rgba(148,163,184,.6)'],
borderWidth:0, hoverOffset:4 }]
},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{position:'right',labels:{font:{size:11},boxWidth:10}}} }
});
}
function nocAlertItemHTML(issue) {
const f = issue.fields||issue; // supporta sia raw issue che oggetto semplice
const key = issue.key||issue._key||'–';
const pri = f.priority?.name || issue.priority || 'Medium';
const priBadge = pri==='Critical'?'badge-red':pri==='High'?'badge-orange':'badge-blue';
const sla = issue._sla || {};
const slaEl = sla.close_pct != null ? `${sla.close_pct}%` : '';
const isFP = (f.customfield_nh_section?.value||'') === 'FalsePositive';
return `
${key}
${f.summary||'–'} ${isFP?'FP':''}
${pri}
${f.customfield_alert_source||issue.source||'–'}
${f.customfield_ci_hostname||issue.hostname ? `${f.customfield_ci_hostname||issue.hostname}` : ''}
${slaEl}
${!isFP ? `` : ''}
`;
}
async function loadNOCAlerts() {
const el = document.getElementById('noc-alerts-list');
el.innerHTML = '';
const pri = document.getElementById('noc-alert-pri')?.value || '';
const source = document.getElementById('noc-alert-source')?.value || '';
const period = document.getElementById('noc-period')?.value || '72';
try {
const r = await workerGet(`/api/noc/events?project=${P()}&priority=${pri}&source=${source}&period=${period}`);
const items = r.issues || [];
document.getElementById('noc-fp-stats').textContent = `FP rate: ${r.fp_rate_pct??0}% (${r.fp_count??0}/${r.total??0})`;
el.innerHTML = items.length === 0
? ''
: items.map(i => nocAlertItemHTML(i)).join('');
} catch(e) { el.innerHTML = ''; }
}
async function loadNOCAvailability() {
const period = document.getElementById('noc-avail-period')?.value || '30';
try {
const r = await workerGet(`/api/noc/availability?project=${P()}&period=${period}`);
const services = r.services || [];
// Grafico availability
if (charts['noc-avail']) { charts['noc-avail'].destroy(); delete charts['noc-avail']; }
const ctx1 = document.getElementById('chart-noc-avail')?.getContext('2d');
if (ctx1 && services.length) {
charts['noc-avail'] = new Chart(ctx1, {
type:'bar', data:{ labels:services.map(s=>s.service),
datasets:[{ label:'Availability %', data:services.map(s=>s.availability_pct),
backgroundColor:services.map(s=>s.availability_pct>=99.9?'rgba(34,197,94,.7)':s.availability_pct>=99?'rgba(249,115,22,.7)':'rgba(239,68,68,.7)'),
borderRadius:4 }]
},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{ y:{min:98,max:100,ticks:{font:{size:10}}}, x:{ticks:{font:{size:10}}} }
}
});
}
// Grafico MTTR
if (charts['noc-mttr']) { charts['noc-mttr'].destroy(); delete charts['noc-mttr']; }
const ctx2 = document.getElementById('chart-noc-mttr')?.getContext('2d');
if (ctx2 && services.length) {
charts['noc-mttr'] = new Chart(ctx2, {
type:'bar', data:{ labels:services.map(s=>s.service),
datasets:[{ label:'MTTR (h)', data:services.map(s=>s.mttr_avg_h),
backgroundColor:'rgba(59,130,246,.7)', borderRadius:4 }]
},
options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{ x:{beginAtZero:true,ticks:{font:{size:10}}}, y:{ticks:{font:{size:10}}} }
}
});
}
// Tabella
const el = document.getElementById('noc-avail-table');
el.innerHTML = services.length === 0
? ''
: `
| Servizio |
Availability |
Downtime |
Incident |
MTTR |
MTBF |
${services.map((s,i)=>`
| ${s.service} |
${s.availability_pct}% |
${s.downtime_h}h |
${s.incidents} |
${s.mttr_avg_h}h |
${s.mtbf_days}d |
`).join('')}
`;
} catch(e) { console.error('loadNOCAvailability', e); }
}
async function loadNOCRunbook(issuetype, priority) {
const panel = document.getElementById('noc-runbook-panel');
panel.innerHTML = '';
try {
const r = await workerGet(`/api/noc/runbook?project=${P()}&issuetype=${issuetype}&priority=${priority}`);
renderRunbook(panel, r);
} catch(e) { panel.innerHTML = ''; }
}
async function generateNOCRunbookAI() {
const key = document.getElementById('noc-runbook-key')?.value.trim();
if (!key) { nhToast('Inserisci una chiave ticket', 'warning'); return; }
const panel = document.getElementById('noc-runbook-panel');
panel.innerHTML = 'Generazione AI in corso...
';
try {
const r = await workerPost('/api/ai-analyze', { type:'runbook-gen', project:P(), context:`Ticket: ${key}` });
if (r.result) renderRunbook(panel, r.result);
else panel.innerHTML = '';
} catch(e) { panel.innerHTML = ''; }
}
function renderRunbook(container, rb) {
const steps = rb.steps || rb.runbook?.steps || [];
container.innerHTML = `
${rb.title||rb.runbook_title||'Runbook'}
⏱ ${rb.estimated_min||rb.runbook?.estimated_resolution_min||'?'} min
👤 ${rb.required_level||rb.required_access_level||'N/D'}
${rb.escalation_path ? `📢 ${rb.escalation_path}` : ''}
${steps.map((s,i) => `
${s.step||i+1}
${s.action||s.action||'–'}
${s.expected_result||s.expected ? `
✓ ${s.expected_result||s.expected}
` : ''}
${s.if_fail ? `
⚠ Se fallisce: ${s.if_fail}
` : ''}
${(s.tools||s.tools_needed||[]).length ? `
${(s.tools||s.tools_needed).map(t=>`${t}`).join('')}
` : ''}
`).join('')}
`;
}
function markRunbookStep(checkbox, idx) {
const num = document.getElementById(`rs-num-${idx}`);
if (num) num.classList.toggle('done', checkbox.checked);
}
async function loadNOCHandoverData() {
const el = document.getElementById('noc-handover-data');
el.innerHTML = '';
try {
const r = await workerGet(`/api/noc/shift-handover?project=${P()}`);
el.innerHTML = `
ALERT APERTI (${r.open_events?.length||0})
${(r.open_events||[]).slice(0,5).map(e=>`
${e.key}
${e.summary}
${e.age_min}min
`).join('') || '
Nessun alert aperto
'}
INCIDENT APERTI (${r.open_incidents?.length||0})
${(r.open_incidents||[]).slice(0,5).map(i=>`
${i.key}
${i.summary}
${i.age_h}h
`).join('') || '
Nessun incident aperto
'}
RISOLTI NELLE ULTIME 8H (${r.resolved_last_8h?.length||0})
${(r.resolved_last_8h||[]).slice(0,5).map(i=>`
${i.key}
${i.summary}
${i.owner||'–'}
`).join('') || '
Nessun ticket risolto
'}
`;
} catch(e) { el.innerHTML = 'Errore caricamento dati turno
'; }
}
async function generateHandoverAI() {
const el = document.getElementById('noc-handover-ai');
el.innerHTML = 'Claude sta analizzando la situazione...
';
try {
const r = await workerPost('/api/ops/ai-shift-handover', { project: P() });
const h = r.handover || {};
el.innerHTML = `
📋 BRIEFING TURNO — ${new Date().toLocaleTimeString('it-IT')}
${h.briefing_text||'Briefing non disponibile'}
${h.priority_items?.length ? `
⚡ AZIONI PRIORITARIE
${h.priority_items.map(p=>`
${p.urgency}
${p.key}
${p.action}
`).join('')}
` : ''}
${h.recommended_first_action ? `💡 Prima azione raccomandata: ${h.recommended_first_action}
` : ''}`;
} catch(e) { el.innerHTML = 'Errore generazione briefing
'; }
}
async function loadNOCPredictive() {
const el = document.getElementById('noc-predictive-content');
el.innerHTML = '';
try {
const r = await workerGet(`/api/ops/predictive?project=${P()}`);
const dowChart = `
`;
const hourChart = `
`;
el.innerHTML = `
Giorno più critico
${r.peak_day_of_week||'–'}
Ora di picco
${r.peak_hour!=null?r.peak_hour+':00':'–'}
Trend 4 settimane
${r.trend_pct_last4w>0?'+':''}${r.trend_pct_last4w||0}%
Valutazione
${r.recommendation||'–'}
${dowChart}
${hourChart}
`;
// Grafici
setTimeout(() => {
const ctx1 = document.getElementById('chart-noc-dow')?.getContext('2d');
if (ctx1 && r.by_day_of_week) {
new Chart(ctx1, { type:'bar', data:{ labels:r.by_day_of_week.map(d=>d.day), datasets:[{ data:r.by_day_of_week.map(d=>d.count), backgroundColor:'rgba(249,115,22,.7)', borderRadius:4 }]}, options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{ticks:{font:{size:10}}},y:{beginAtZero:true,ticks:{font:{size:10}}}}}});
}
const ctx2 = document.getElementById('chart-noc-hour')?.getContext('2d');
if (ctx2 && r.by_hour) {
new Chart(ctx2, { type:'bar', data:{ labels:r.by_hour.map(h=>h.hour+':00'), datasets:[{ data:r.by_hour.map(h=>h.count), backgroundColor:r.by_hour.map(h=>h.hour===r.peak_hour?'rgba(239,68,68,.8)':'rgba(59,130,246,.5)'), borderRadius:4 }]}, options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{ticks:{font:{size:9}}},y:{beginAtZero:true,ticks:{font:{size:10}}}}}});
}
}, 100);
} catch(e) { el.innerHTML = 'Errore caricamento analisi predittiva
'; }
}
async function markFP(key) {
const reason = prompt('Motivo falso positivo (obbligatorio):', 'Attività legittima');
if (!reason) return;
try {
await workerPost('/api/noc/mark-false-positive', { project: P(), key, reason });
nhToast(`${key} marcato come Falso Positivo`, 'success');
loadNOCAlerts();
} catch(e) { nhToast('Errore marcatura FP', 'error'); }
}
// ══════════════════════════════════════════════════════════
// SOC FUNCTIONS
// ══════════════════════════════════════════════════════════
function showSOCTab(tab) {
const tabs = ['overview','alerts','vulns','mitre','compliance','workload','timeline'];
document.querySelectorAll('#page-soc .noc-soc-tab').forEach((t,i) => {
t.classList.toggle('active', tabs[i] === tab);
});
document.querySelectorAll('#page-soc .noc-soc-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById(`soc-panel-${tab}`);
if (panel) panel.classList.add('active');
if (tab === 'alerts') loadSOCAlerts();
if (tab === 'vulns') loadSOCVulns();
if (tab === 'mitre') loadSOCMitre();
if (tab === 'compliance') loadSOCCompliance();
if (tab === 'workload') loadSOCWorkload();
}
async function loadSOCDashboard() {
try {
const r = await workerGet(`/api/soc/dashboard?project=${P()}`);
const byS = r.vuln_by_severity || {};
document.getElementById('soc-k-alerts').textContent = r.alerts_open ?? '–';
document.getElementById('soc-k-incidents').textContent = r.incidents_open ?? '–';
document.getElementById('soc-k-vuln-crit').textContent = byS.Critical ?? '–';
document.getElementById('soc-k-exploit').textContent = r.exploitable_vulns ?? '–';
document.getElementById('soc-k-fp').textContent = r.fp_rate_30d_pct ?? '–';
// Badge sidebar
const badge = document.getElementById('soc-badge');
if (badge && r.alerts_open > 0) { badge.style.display='inline'; badge.textContent = r.alerts_open; }
// SLA banner per alert critici non triage
if (r.p1_alerts?.length > 0) {
const banner = document.getElementById('soc-sla-banner');
const bannerText = document.getElementById('soc-sla-banner-text');
if (banner && bannerText) {
banner.style.display = 'flex';
bannerText.textContent = `🔴 ${r.p1_alerts.length} SOC-ALERT P1 non risolti — triage entro 15 minuti (SLA SOC)`;
}
}
// Grafico vuln per severity
buildSOCVulnChart(r.vuln_by_severity||{});
// Grafico alert per tipo minaccia
buildSOCThreatChart(r.alerts_by_threat||[]);
// P1 list
const p1El = document.getElementById('soc-p1-list');
p1El.innerHTML = (r.p1_alerts||[]).length === 0
? ''
: (r.p1_alerts||[]).map(a => `
${a.key}
${a.summary||'–'}
Critical
${a.threat||'–'}
${a.created?.split('T')[0]||''}
`).join('');
// Top assets
const assEl = document.getElementById('soc-top-assets');
const assets = r.top_vulnerable_assets||[];
assEl.innerHTML = assets.length === 0
? ''
: assets.map((a,i) => `
#${i+1}
${a.host}
${a.vuln_count} vuln
CVSS ${a.max_cvss}
`).join('');
} catch(e) { console.error('loadSOCDashboard', e); }
}
function buildSOCVulnChart(sevData) {
if (charts['soc-vuln']) { charts['soc-vuln'].destroy(); delete charts['soc-vuln']; }
const canvas = document.getElementById('chart-soc-vuln');
if (!canvas) return;
charts['soc-vuln'] = new Chart(canvas.getContext('2d'), {
type:'doughnut',
data:{ labels:['Critical','High','Medium','Low'],
datasets:[{ data:[sevData.Critical||0,sevData.High||0,sevData.Medium||0,sevData.Low||0],
backgroundColor:['rgba(239,68,68,.85)','rgba(249,115,22,.85)','rgba(234,179,8,.7)','rgba(148,163,184,.5)'],
borderWidth:0, hoverOffset:6 }]
},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{position:'right',labels:{font:{size:11},boxWidth:10}}} }
});
}
function buildSOCThreatChart(threatsData) {
if (charts['soc-threat']) { charts['soc-threat'].destroy(); delete charts['soc-threat']; }
const canvas = document.getElementById('chart-soc-threat');
if (!canvas || !threatsData.length) return;
const top = threatsData.slice(0,7);
charts['soc-threat'] = new Chart(canvas.getContext('2d'), {
type:'bar',
data:{ labels:top.map(t=>t.threat),
datasets:[{ data:top.map(t=>t.count), backgroundColor:'rgba(239,68,68,.7)', borderRadius:4 }]
},
options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{ x:{beginAtZero:true,ticks:{font:{size:10}}}, y:{ticks:{font:{size:10}}} }
}
});
}
async function loadSOCAlerts() {
const el = document.getElementById('soc-alerts-list');
el.innerHTML = '';
const pri = document.getElementById('soc-alert-pri')?.value || '';
const threat = document.getElementById('soc-alert-threat')?.value || '';
try {
const r = await workerGet(`/api/soc/alerts?project=${P()}&priority=${pri}&threat_type=${encodeURIComponent(threat)}`);
const items = r.issues || [];
el.innerHTML = items.length === 0
? ''
: items.map(i => socAlertItemHTML(i)).join('');
} catch(e) { el.innerHTML = ''; }
}
function socAlertItemHTML(issue) {
const f = issue.fields||{};
const pri = f.priority?.name||'Medium';
const priBadge = pri==='Critical'?'badge-red':pri==='High'?'badge-orange':'badge-blue';
const sla = issue._sla||{};
const slaEl = sla.close_pct!=null ? `SLA ${sla.close_pct}%` : '';
const mitre = f.customfield_soc_mitre_technique||'';
return `
${issue.key}
${f.summary||'–'}
${pri}
${f.customfield_threat_type?.value||f.customfield_soc_threat_type?.value||'–'}
${f.customfield_ci_hostname ? `${f.customfield_ci_hostname}` : ''}
${mitre ? `${mitre}` : ''}
${slaEl}
`;
}
async function runAITriage(key) {
nhToast(`AI Triage in corso per ${key}...`, 'info');
try {
const r = await workerPost('/api/ops/ai-triage', { project:P(), key });
const t = r.triage||{};
const msg = `${key}: ${t.is_false_positive?'⚠ Possibile FP ('+Math.round(t.fp_probability*100)+'%)':'✓ True Positive'} | Priorità: ${t.confirmed_priority||'N/D'} | ${t.immediate_actions?.[0]||''}`;
nhToast(msg, t.is_false_positive?'warning':'info');
// Mostra pannello AI triage nella modal
const modalBody = document.getElementById('modal-ticket-body');
if (modalBody && document.getElementById('modal-ticket').classList.contains('open')) {
const triageHtml = `
🤖 AI TRIAGE RESULT
${t.is_false_positive?'Possibile FP '+Math.round(t.fp_probability*100)+'%':'True Positive'}
Priorità: ${t.confirmed_priority||'N/D'}
Confidenza: ${Math.round(t.confidence*100)||'?'}%
${t.immediate_actions?.length ? `
Azioni immediate:${t.immediate_actions.map(a=>`- ${a}
`).join('')}
` : ''}
${t.similar_tickets?.length ? `
Ticket simili: ${t.similar_tickets.map(s=>`${s.key} (${s.similarity_pct}%)`).join(', ')}
` : ''}
${t.compliance_flags?.length ? `
⚠ Flag compliance: ${t.compliance_flags.join(', ')}
` : ''}
`;
modalBody.insertAdjacentHTML('beforeend', triageHtml);
}
} catch(e) { nhToast('Errore AI triage', 'error'); }
}
async function loadSOCVulns() {
const el = document.getElementById('soc-vulns-list');
el.innerHTML = '';
const sev = document.getElementById('soc-vuln-sev')?.value||'';
const exploit = document.getElementById('soc-vuln-exploit')?.value||'';
try {
let jql = `project="${P()}" AND issuetype="${P()}-SOC-VULN" AND statusCategory!=Done`;
if (sev) jql += ` AND priority="${sev}"`;
if (exploit) jql += ` AND "SOC-Exploit-Available[Select List (single choice)]"="Yes"`;
jql += ` ORDER BY "SOC-CVSS-Score[Number]" DESC, priority ASC`;
const r = await workerPost('/api/search', { jql, fields:['summary','status','priority','created','customfield_soc_cve_id','customfield_soc_cvss_score','customfield_soc_affected_host','customfield_soc_remediation_due','customfield_soc_exploit_available','customfield_soc_threat_severity','customfield_soc_mitre_technique'], maxResults:100 });
const kpiEl = document.getElementById('soc-vuln-kpis');
const issues = r.issues||[];
const crit = issues.filter(i=>i.fields.priority?.name==='Critical').length;
const high = issues.filter(i=>i.fields.priority?.name==='High').length;
const expl = issues.filter(i=>i.fields.customfield_soc_exploit_available?.value==='Yes').length;
const past = issues.filter(i=>{const d=i.fields.customfield_soc_remediation_due; return d&&new Date(d)Critical
${crit}
`;
el.innerHTML = issues.length===0
? '✅
Nessuna vulnerabilità trovata
'
: `
| CVE |
Host |
CVSS |
Severity |
Exploit |
Remediation Due |
MITRE |
${issues.map((i,idx)=>{
const f=i.fields;
const cvss=parseFloat(f.customfield_soc_cvss_score||0);
const due=f.customfield_soc_remediation_due;
const isPast=due&&new Date(due)
${f.customfield_soc_cve_id||i.key} |
${f.customfield_soc_affected_host||'–'} |
${cvss||'–'} |
${f.priority?.name||'–'} |
${expl||'–'} |
${due||'–'} |
${f.customfield_soc_mitre_technique||'–'} |
`;
}).join('')}
`;
} catch(e) { el.innerHTML = ''; }
}
async function loadSOCMitre() {
const el = document.getElementById('mitre-heatmap');
const topEl = document.getElementById('mitre-top-list');
el.innerHTML = '';
try {
const r = await workerGet(`/api/soc/mitre?project=${P()}`);
const techniques = r.techniques||[];
if (!techniques.length) {
el.innerHTML = '🛡️
Nessuna tecnica MITRE rilevata
I ticket SOC con campo MITRE-Technique compilato appariranno qui
';
topEl.innerHTML = el.innerHTML;
return;
}
// Tattiche MITRE in ordine kill chain
const TACTICS = ['Reconnaissance','Resource Development','Initial Access','Execution','Persistence','Privilege Escalation','Defense Evasion','Credential Access','Discovery','Lateral Movement','Collection','Command and Control','Exfiltration','Impact'];
const maxCount = Math.max(...techniques.map(t=>t.count));
const levelClass = c => c===0?'l0':c<=2?'l1':c<=5?'l2':'l3';
// Heatmap semplificata per tecnica
const rows = techniques.slice(0,30).map(t => `
${t.technique}
${t.count}
${t.severity_max}
`).join('');
el.innerHTML = `${rows}
Clicca su una tecnica per filtrare gli alert correlati
`;
// Top list
topEl.innerHTML = `
| # |
Tecnica MITRE |
Ticket |
Max Severity |
Ticket correlati |
${techniques.slice(0,10).map((t,i)=>`
| #${i+1} |
${t.technique} |
${t.count} |
${t.severity_max} |
${(t.tickets||[]).slice(0,4).join(', ')}${t.tickets?.length>4?'...':''} |
`).join('')}
`;
} catch(e) { el.innerHTML = ''; }
}
function filterSOCAlertsBy(type, value) {
showSOCTab('alerts');
if (type === 'mitre') nhToast(`Filtro MITRE ${value} — funzione in sviluppo`, 'info');
}
async function loadSOCCompliance() {
const fw = document.getElementById('soc-compliance-fw')?.value||'all';
const ringsEl = document.getElementById('soc-compliance-rings');
const detailEl = document.getElementById('soc-compliance-detail');
ringsEl.innerHTML = '';
try {
const r = await workerGet(`/api/soc/compliance?project=${P()}&framework=${fw}`);
const fws = r.frameworks||{};
const FW_LABELS = {iso27001:'ISO 27001',nis2:'NIS2',gdpr:'GDPR',dora:'DORA',pci_dss:'PCI-DSS',soc2:'SOC2'};
// Score rings
const fwList = Object.entries(fws);
ringsEl.innerHTML = fwList.map(([k,fw])=>{
const circ=157; const pct=fw.score||0;
const offset=circ-(circ*pct/100);
const color=pct>=90?'var(--green)':pct>=70?'var(--orange)':'var(--red)';
return `
${FW_LABELS[k]||k}
${fw.key_gaps?.length ? `
${fw.key_gaps.length} gap
` : '
✓ OK
'}
`;
}).join('');
// Overall score
if (r.overall_score) {
ringsEl.insertAdjacentHTML('afterend', `Score complessivo: ${r.overall_score}%
`);
}
// Dettaglio gap per framework
detailEl.innerHTML = fwList.map(([k,fw])=>`
${fw.key_gaps?.length ? `
${fw.key_gaps.map(g=>`
⚠
${g}
`).join('')}
` : '
✅ Nessun gap critico rilevato
'}
${fw.nis2_notification!==undefined ? `
Notifiche NIS2 entro 24h: ${fw.notification_24h_rate}%
` : ''}
${fw.breach_notifications_72h!==undefined ? `
Data breach GDPR ≤72h: ${fw.breach_notifications_72h}% (${fw.data_breach_incidents} incident, ${fw.breaches_late} tardivi)
` : ''}
`).join('');
} catch(e) {
ringsEl.innerHTML = 'Errore caricamento compliance
';
}
}
async function loadSOCWorkload() {
const workEl = document.getElementById('soc-workload-table');
const unassEl = document.getElementById('soc-unassigned-list');
const predEl = document.getElementById('soc-workload-predict');
workEl.innerHTML = '';
try {
const [wlRes, predRes] = await Promise.all([
workerGet(`/api/soc/workload?project=${P()}`),
workerGet(`/api/ops/predictive?project=${P()}`),
]);
const ops = wlRes.operators||[];
workEl.innerHTML = ops.length===0
? ''
: `
| Operatore |
Aperti |
Risolti |
MTTR |
FP% |
Carico |
${ops.map((op,i)=>{
const maxOpen=Math.max(...ops.map(o=>o.open),1);
return `
| ${op.name} |
${op.open} |
${op.resolved} |
${op.mttr_avg_h||'–'}h |
${op.fp_rate_pct}% |
|
`;
}).join('')}
`;
unassEl.innerHTML = (wlRes.unassigned_tickets||0) === 0
? '✅
Nessun ticket non assegnato
'
: `${wlRes.unassigned_tickets}
ticket richiedono assegnazione
`;
predEl.innerHTML = `
📅 Giorno più critico: ${predRes.peak_day_of_week||'–'}
⏰ Ora di picco: ${predRes.peak_hour!=null?predRes.peak_hour+':00':'–'}
📈 Trend 4 settimane: ${predRes.trend_pct_last4w>0?'+':''}${predRes.trend_pct_last4w||0}%
${predRes.recommendation||''}
`;
} catch(e) { workEl.innerHTML = ''; }
}
async function loadSOCTimeline() {
const key = document.getElementById('soc-timeline-key')?.value.trim();
if (!key) { nhToast('Inserisci la chiave del ticket', 'warning'); return; }
const el = document.getElementById('soc-timeline-content');
el.innerHTML = '';
try {
const r = await workerGet(`/api/soc/timeline?project=${P()}&key=${key}`);
const evts = r.timeline||[];
el.innerHTML = `
${r.priority||'–'}
${r.status||'–'}
${r.threat_type ? `${r.threat_type}` : ''}
${r.mitre ? `${r.mitre}` : ''}
${r.mttr_h ? `MTTR: ${Math.round(r.mttr_h*10)/10}h` : ''}
${r.linked_tickets?.length ? `Ticket correlati: ${r.linked_tickets.map(l=>`${l.key}`).join(', ')}
` : ''}
${evts.map(e=>`
${new Date(e.ts).toLocaleString('it-IT',{day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'})}
${e.text||'–'}
${e.actor||''}
`).join('')}
`;
} catch(e) { el.innerHTML = 'Ticket non trovato o errore
'; }
}
// ══════════════════════════════════════════════════════════
// KB AUTO-GENERATION
// ══════════════════════════════════════════════════════════
async function generateKBFromTicket(key) {
if (!key) {
key = prompt('Chiave del ticket risolto (es. NHIT-123):');
if (!key) return;
}
nhToast(`Generazione KB da ${key} in corso...`, 'info');
try {
const r = await workerPost('/api/ops/kb-from-ticket', { project:P(), key });
if (!r.ok) { nhToast('Errore generazione KB', 'error'); return; }
const kb = r.kb_draft||{};
// Mostra modal con bozza KB
document.getElementById('modal-ticket-key').textContent = `KB Draft — ${key}`;
document.getElementById('modal-ticket-body').innerHTML = `
${kb.title||key}
${kb.summary||''}
${(kb.tags||[]).map(t=>`${t}`).join('')}
CAUSA RADICE
${kb.root_cause||'–'}
PASSI DI RISOLUZIONE
${(kb.resolution_steps||[]).map(s=>`
${s.step}.
${s.action} → ${s.expected_result}
`).join('')}
${kb.workaround ? `💡 Workaround: ${kb.workaround}
` : ''}
`;
openModal('modal-ticket');
nhToast('Bozza KB generata con successo', 'success');
} catch(e) { nhToast('Errore generazione KB', 'error'); }
}
async function saveKBDraft(sourceKey) {
nhToast('Salvataggio KB in Jira...', 'info');
// Crea un ticket Known-Error in Jira con la bozza
try {
const r = await workerPost('/api/issue', { body:{ fields:{
project:{key:P()}, issuetype:{name:`${P()}-IT-KE`},
summary:`[KB Draft] ${sourceKey}`, status:'To Do',
}}});
if (r.ok) nhToast(`KB creata: ${r.key}`, 'success');
closeModal('modal-ticket');
} catch(e) { nhToast('Errore salvataggio KB', 'error'); }
}
// ══════════════════════════════════════════════════════════
// TIER 3 — PERFORMANCE, GAMIFICATION, PREDICTIVE AVANZATO
// ══════════════════════════════════════════════════════════
// ── ACHIEVEMENT DEFINITIONS ──────────────────────────────
const ACHIEVEMENTS = [
// SLA
{ id:'sla_master', icon:'🎯', name:'SLA Master', desc:'0% SLA breach nel periodo', category:'sla', check:(d) => d.sla_breach_rate_pct === 0 && d.resolved > 5 },
{ id:'sla_champion', icon:'⚡', name:'SLA Champion', desc:'SLA breach < 5% su 20+ ticket', category:'sla', check:(d) => d.sla_breach_rate_pct < 5 && d.resolved >= 20 },
// Velocità
{ id:'speed_demon', icon:'🚀', name:'Speed Demon', desc:'MTTR medio < 2 ore', category:'speed', check:(d) => d.mttr_avg_h != null && d.mttr_avg_h < 2 && d.resolved >= 5 },
{ id:'quick_resolver', icon:'⚡', name:'Quick Resolver', desc:'MTTR medio < 4 ore', category:'speed', check:(d) => d.mttr_avg_h != null && d.mttr_avg_h < 4 && d.resolved >= 10 },
// Qualità
{ id:'fp_hunter', icon:'🔍', name:'FP Hunter', desc:'FP rate < 5% su 20+ alert', category:'quality', check:(d) => d.fp_rate_pct < 5 && d.total_tickets >= 20 },
{ id:'zero_fp', icon:'🎪', name:'Zero FP Month', desc:'0 falsi positivi nel periodo', category:'quality', check:(d) => d.false_positives === 0 && d.total_tickets >= 10 },
// Volume
{ id:'ticket_hero', icon:'💪', name:'Ticket Hero', desc:'50+ ticket risolti nel periodo', category:'volume', check:(d) => d.resolved >= 50 },
{ id:'centurion', icon:'🏛️', name:'Centurion', desc:'100+ ticket risolti nel periodo', category:'volume', check:(d) => d.resolved >= 100 },
// NOC/SOC
{ id:'noc_guardian', icon:'📡', name:'NOC Guardian', desc:'Gestito 10+ alert NOC P1', category:'noc', check:(d) => (d.by_priority?.find(p=>p.priority==='Critical')?.count||0) >= 10 },
{ id:'soc_analyst', icon:'🛡️', name:'SOC Analyst', desc:'Chiuso 5+ SOC-INCIDENT', category:'soc', check:(d) => d.resolved >= 5 && d.total_tickets >= 5 },
// Streak
{ id:'consistent', icon:'📈', name:'Consistent', desc:'Ticket risolti ogni giorno lavorativo', category:'streak', check:(d) => d.resolved >= 20 && d.sla_breach_rate_pct < 10 },
// Knowledge
{ id:'kb_author', icon:'📚', name:'KB Author', desc:'Creato 3+ articoli KB', category:'kb', check:(d) => (d.by_priority?.length||0) >= 3 }, // proxy: se ha dati
{ id:'mentor', icon:'🎓', name:'Mentor', desc:'MTTR migliorato del 20% vs mese precedente', category:'growth', check:(d) => d.mttr_avg_h != null && d.mttr_avg_h < 3 },
// Special
{ id:'first_responder',icon:'🚨', name:'First Responder', desc:'Preso in carico P1 entro 5 minuti', category:'special', check:(d) => d.total_tickets >= 1 },
{ id:'night_owl', icon:'🦉', name:'Night Owl', desc:'Risolto ticket fuori orario business', category:'special', check:(d) => d.resolved >= 1 },
];
function calcAchievements(perfData) {
return ACHIEVEMENTS.map(a => ({
...a,
earned: a.check(perfData),
progress: Math.min(100, a.check(perfData) ? 100 : Math.round((perfData.resolved || 0) / 10 * 100)),
}));
}
// ── PERFORMANCE PAGE ──────────────────────────────────────
function showPerfTab(tab) {
const tabs = ['overview','leaderboard','achievements','predictive','personal'];
document.querySelectorAll('#page-performance .noc-soc-tab').forEach((t,i) => {
t.classList.toggle('active', tabs[i] === tab);
});
document.querySelectorAll('#page-performance .noc-soc-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById(`perf-panel-${tab}`);
if (panel) panel.classList.add('active');
if (tab === 'leaderboard') loadLeaderboard();
if (tab === 'achievements') loadAchievements();
if (tab === 'predictive') loadPredictiveAdvanced();
if (tab === 'personal') loadMyPerformance();
}
async function loadPerformance() {
const period = document.getElementById('perf-period')?.value || '30';
try {
const r = await workerGet(`/api/soc/workload?project=${P()}`);
const ops = r.operators || [];
// KPI team
const totalResolved = ops.reduce((s,o) => s + o.resolved, 0);
const avgMttr = ops.filter(o=>o.mttr_avg_h).reduce((s,o)=>s+o.mttr_avg_h,0) / (ops.filter(o=>o.mttr_avg_h).length||1);
const avgFp = ops.reduce((s,o)=>s+o.fp_rate_pct,0) / (ops.length||1);
const kpiEl = document.getElementById('perf-team-kpis');
kpiEl.innerHTML = `
Operatori attivi
${ops.length}
Ticket risolti team
${totalResolved}
ultimi 30g
MTTR medio team
${Math.round(avgMttr*10)/10||'–'}
ore
FP Rate medio
${Math.round(avgFp)}%
`;
// Grafici overview
buildPerfCharts(ops);
} catch(e) { console.error('loadPerformance', e); }
}
function buildPerfCharts(ops) {
const names = ops.map(o => o.name.split(' ')[0]); // solo nome
const colors = ['rgba(59,130,246,.7)','rgba(34,197,94,.7)','rgba(249,115,22,.7)','rgba(167,139,250,.7)','rgba(0,212,180,.7)','rgba(239,68,68,.7)'];
const bgColors = names.map((_,i) => colors[i % colors.length]);
// MTTR
if (charts['perf-mttr']) { charts['perf-mttr'].destroy(); delete charts['perf-mttr']; }
const ctx1 = document.getElementById('chart-perf-mttr')?.getContext('2d');
if (ctx1) {
charts['perf-mttr'] = new Chart(ctx1, {
type:'bar',
data:{ labels:names, datasets:[{ data:ops.map(o=>o.mttr_avg_h||0), backgroundColor:bgColors, borderRadius:4 }]},
options:{ indexAxis:'y', responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{ x:{beginAtZero:true,ticks:{font:{size:10}},grid:{color:'rgba(128,128,128,.08)'}}, y:{ticks:{font:{size:10}},grid:{display:false}} }
}
});
}
// Risolti
if (charts['perf-resolved']) { charts['perf-resolved'].destroy(); delete charts['perf-resolved']; }
const ctx2 = document.getElementById('chart-perf-resolved')?.getContext('2d');
if (ctx2) {
charts['perf-resolved'] = new Chart(ctx2, {
type:'bar',
data:{ labels:names, datasets:[{ data:ops.map(o=>o.resolved), backgroundColor:bgColors, borderRadius:4 }]},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{ x:{ticks:{font:{size:10}},grid:{display:false}}, y:{beginAtZero:true,ticks:{font:{size:10}}} }
}
});
}
// FP Rate
if (charts['perf-fp']) { charts['perf-fp'].destroy(); delete charts['perf-fp']; }
const ctx3 = document.getElementById('chart-perf-fp')?.getContext('2d');
if (ctx3) {
charts['perf-fp'] = new Chart(ctx3, {
type:'bar',
data:{ labels:names, datasets:[{ data:ops.map(o=>o.fp_rate_pct), backgroundColor:ops.map(o=>o.fp_rate_pct>20?'rgba(239,68,68,.7)':o.fp_rate_pct>10?'rgba(249,115,22,.7)':'rgba(34,197,94,.7)'), borderRadius:4 }]},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{ x:{ticks:{font:{size:10}},grid:{display:false}}, y:{beginAtZero:true,ticks:{font:{size:10},callback:v=>v+'%'}} }
}
});
}
// SLA Breach
if (charts['perf-sla']) { charts['perf-sla'].destroy(); delete charts['perf-sla']; }
const ctx4 = document.getElementById('chart-perf-sla')?.getContext('2d');
if (ctx4) {
charts['perf-sla'] = new Chart(ctx4, {
type:'bar',
data:{ labels:names, datasets:[{ data:ops.map(o=>o.sla_breach_rate_pct||0), backgroundColor:ops.map(o=>(o.sla_breach_rate_pct||0)>15?'rgba(239,68,68,.7)':(o.sla_breach_rate_pct||0)>5?'rgba(249,115,22,.7)':'rgba(34,197,94,.7)'), borderRadius:4 }]},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{ x:{ticks:{font:{size:10}},grid:{display:false}}, y:{beginAtZero:true,ticks:{font:{size:10},callback:v=>v+'%'}} }
}
});
}
}
// ── LEADERBOARD ───────────────────────────────────────────
async function loadLeaderboard() {
try {
const r = await workerGet(`/api/soc/workload?project=${P()}`);
const ops = r.operators || [];
const buildLB = (sorted, valueKey, valueLabel, colorFn) => {
return sorted.slice(0,5).map((op,i) => {
const val = valueKey === 'mttr' ? (op.mttr_avg_h!=null ? op.mttr_avg_h+'h' : 'N/D')
: valueKey === 'fp' ? op.fp_rate_pct+'%'
: valueKey === 'sla' ? (100-(op.sla_breach_rate_pct||0))+'%'
: op.resolved;
const medal = i===0?'🥇':i===1?'🥈':i===2?'🥉':'';
const rankClass = i===0?'r1':i===1?'r2':i===2?'r3':'';
return `
${medal||'#'+(i+1)}
${op.name}
${val}
${valueLabel}
`;
}).join('');
};
const byResolved = [...ops].sort((a,b) => b.resolved - a.resolved);
const byMTTR = [...ops].filter(o=>o.mttr_avg_h!=null).sort((a,b) => a.mttr_avg_h - b.mttr_avg_h);
const bySLA = [...ops].sort((a,b) => (a.sla_breach_rate_pct||0) - (b.sla_breach_rate_pct||0));
const byFP = [...ops].sort((a,b) => a.fp_rate_pct - b.fp_rate_pct);
document.getElementById('leaderboard-resolved').innerHTML = buildLB(byResolved, 'resolved', 'ticket risolti', op=>'var(--green)') || '';
document.getElementById('leaderboard-mttr').innerHTML = buildLB(byMTTR, 'mttr', 'ore medio', op=>op.mttr_avg_h<2?'var(--green)':op.mttr_avg_h<6?'var(--orange)':'var(--red)');
document.getElementById('leaderboard-sla').innerHTML = buildLB(bySLA, 'sla', 'compliance', op=>(100-(op.sla_breach_rate_pct||0))>=95?'var(--green)':'var(--orange)');
document.getElementById('leaderboard-fp').innerHTML = buildLB(byFP, 'fp', 'FP rate', op=>op.fp_rate_pct<5?'var(--green)':op.fp_rate_pct<15?'var(--orange)':'var(--red)');
} catch(e) { console.error('loadLeaderboard', e); }
}
// ── ACHIEVEMENTS ──────────────────────────────────────────
async function loadAchievements() {
const el = document.getElementById('achievements-grid');
const selEl = document.getElementById('perf-ach-op');
el.innerHTML = '';
try {
const r = await workerGet(`/api/soc/workload?project=${P()}`);
const ops = r.operators || [];
// Popola select operatori
if (selEl && selEl.options.length <= 1) {
ops.forEach(op => {
const opt = document.createElement('option');
opt.value = op.email || op.name;
opt.textContent = op.name;
selEl.appendChild(opt);
});
}
const selectedOp = selEl?.value || '';
// Calcola achievement per tutti o per operatore selezionato
let perfData;
if (selectedOp) {
const opData = ops.find(o => o.email === selectedOp || o.name === selectedOp);
perfData = opData || {};
} else {
// Aggregato team
perfData = {
resolved: ops.reduce((s,o)=>s+o.resolved,0),
total_tickets: ops.reduce((s,o)=>s+o.total,0),
false_positives: ops.reduce((s,o)=>s+o.false_positives,0),
fp_rate_pct: Math.round(ops.reduce((s,o)=>s+o.fp_rate_pct,0)/(ops.length||1)),
mttr_avg_h: ops.filter(o=>o.mttr_avg_h).length ? Math.round(ops.filter(o=>o.mttr_avg_h).reduce((s,o)=>s+o.mttr_avg_h,0)/ops.filter(o=>o.mttr_avg_h).length*10)/10 : null,
sla_breach_rate_pct:Math.round(ops.reduce((s,o)=>s+(o.sla_breach_rate_pct||0),0)/(ops.length||1)),
by_priority: ops[0]?.by_priority || [],
};
}
const achievements = calcAchievements(perfData);
const earned = achievements.filter(a=>a.earned).length;
el.innerHTML = `
${earned}/${achievements.length} badge ottenuti ${selectedOp ? `— ${selectedOp}` : '— Team aggregato'}
` +
achievements.map(a => `
${a.icon}
${a.name}
${a.desc}
${!a.earned ? `
` : ''}
${a.earned ? '
✓' : ''}
`).join('');
} catch(e) { el.innerHTML = ''; }
}
// ── PREDICTIVE AVANZATO ───────────────────────────────────
async function loadPredictiveAdvanced() {
const [predEl, forecastEl, heatmapEl, peaksEl, recEl] = [
'chart-pred-trend','pred-forecast-list','pred-heatmap','pred-peaks','pred-recommendation'
].map(id => document.getElementById(id));
try {
const r = await workerGet(`/api/ops/predictive?project=${P()}`);
// Trend 12 settimane
if (charts['pred-trend']) { charts['pred-trend'].destroy(); delete charts['pred-trend']; }
const ctx = predEl?.getContext('2d');
if (ctx && r.weekly_trend?.length) {
const weeks = r.weekly_trend;
const avgVal = Math.round(weeks.reduce((s,w)=>s+w.count,0)/weeks.length);
charts['pred-trend'] = new Chart(ctx, {
type:'line',
data:{ labels:weeks.map(w=>w.week),
datasets:[
{ label:'Volume reale', data:weeks.map(w=>w.count), borderColor:'#3b82f6', backgroundColor:'rgba(59,130,246,.08)', tension:.4, pointRadius:3, fill:true },
{ label:'Media', data:weeks.map(()=>avgVal), borderColor:'rgba(148,163,184,.4)', borderDash:[4,4], pointRadius:0, fill:false },
]
},
options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{position:'bottom',labels:{font:{size:11},boxWidth:10}}},
scales:{ x:{ticks:{font:{size:9},maxRotation:45},grid:{display:false}}, y:{beginAtZero:true,ticks:{font:{size:10}},grid:{color:'rgba(128,128,128,.08)'}} }
}
});
}
// Forecast prossime 4 settimane (basato su trend)
const trend = r.trend_pct_last4w || 0;
const lastWeeks = r.weekly_trend?.slice(-4) || [];
const avgLast4 = lastWeeks.length ? Math.round(lastWeeks.reduce((s,w)=>s+w.count,0)/lastWeeks.length) : 0;
const forecast = [1,2,3,4].map(i => {
const predicted = Math.round(avgLast4 * (1 + (trend/100) * (i/4)));
return { week:`+${i}w`, predicted, confidence:Math.max(60,95-i*8) };
});
if (forecastEl) {
forecastEl.innerHTML = forecast.map(f => `
${f.week}
${f.predicted} ticket previsti
${f.confidence}% confidence
`).join('');
}
// Heatmap settimana (7 giorni × 24 ore semplificata per giorno)
const DOW = ['Dom','Lun','Mar','Mer','Gio','Ven','Sab'];
const byDow = r.by_day_of_week || [];
const maxDow = Math.max(...byDow.map(d=>d.count),1);
if (heatmapEl) {
heatmapEl.innerHTML = `
${byDow.map(d => {
const pct = Math.round((d.count/maxDow)*100);
const color = pct>75?'var(--red)':pct>50?'var(--orange)':pct>25?'var(--yellow)':'var(--surface-3)';
return `
`;
}).join('')}
`;
}
// Picchi
const byHour = r.by_hour || [];
const topHours = [...byHour].sort((a,b)=>b.count-a.count).slice(0,5);
if (peaksEl) {
peaksEl.innerHTML = `
Ore con più ticket aperti
${topHours.map((h,i) => `
${i===0?'🔴':i===1?'🟠':'🟡'}
${h.hour}:00
${h.count}
`).join('')}`;
}
// Raccomandazione staffing
if (recEl) {
const trendIcon = r.trend_direction==='increasing'?'📈':r.trend_direction==='decreasing'?'📉':'➡️';
recEl.innerHTML = `
${trendIcon}
Trend ${r.trend_pct_last4w>0?'+':''}${r.trend_pct_last4w||0}% (4 settimane)
${r.recommendation||'Dati insufficienti per una previsione'}
Giorno di picco: ${r.peak_day_of_week||'–'} |
Ora di picco: ${r.peak_hour!=null?r.peak_hour+':00':'–'}
`;
}
} catch(e) { console.error('loadPredictiveAdvanced', e); }
}
// ── MY PERFORMANCE ────────────────────────────────────────
async function loadMyPerformance() {
const currentEmail = currentUser?.email || '';
document.getElementById('my-perf-name').textContent = currentUser?.displayName || currentEmail;
try {
const r = await workerGet(`/api/ops/performance?project=${P()}&operator=${encodeURIComponent(currentEmail)}`);
document.getElementById('my-k-total').textContent = r.total_tickets ?? '–';
document.getElementById('my-k-resolved').textContent = r.resolved ?? '–';
document.getElementById('my-k-mttr').textContent = r.mttr_avg_h != null ? r.mttr_avg_h : '–';
document.getElementById('my-k-fp').textContent = r.fp_rate_pct ?? '–';
document.getElementById('my-k-breach').textContent = r.sla_breach_rate_pct ?? '–';
// I miei achievement
const myAchEl = document.getElementById('my-achievements');
const myAch = calcAchievements(r);
const earned = myAch.filter(a=>a.earned);
myAchEl.innerHTML = myAch.map(a => `
${a.icon}
${a.earned ? '
✓' : ''}
`).join('');
// Radar chart — confronto con media team
const teamR = await workerGet(`/api/soc/workload?project=${P()}`);
const ops = teamR.operators || [];
const teamAvgMttr = ops.filter(o=>o.mttr_avg_h).reduce((s,o)=>s+o.mttr_avg_h,0) / (ops.filter(o=>o.mttr_avg_h).length||1);
const teamAvgResolved = ops.reduce((s,o)=>s+o.resolved,0) / (ops.length||1);
const teamAvgFp = ops.reduce((s,o)=>s+o.fp_rate_pct,0) / (ops.length||1);
const teamAvgSla = ops.reduce((s,o)=>s+(o.sla_breach_rate_pct||0),0) / (ops.length||1);
if (charts['my-radar']) { charts['my-radar'].destroy(); delete charts['my-radar']; }
const ctx = document.getElementById('chart-my-radar')?.getContext('2d');
if (ctx) {
// Normalizza su scala 0-100 (più alto = meglio)
const normalize = (val, teamAvg, invert=false) => {
if (!val && val !== 0) return 50;
const ratio = teamAvg > 0 ? val / teamAvg : 1;
const score = invert ? Math.min(100, Math.round((1/ratio)*100)) : Math.min(100, Math.round(ratio*100));
return Math.max(0, Math.min(100, score));
};
charts['my-radar'] = new Chart(ctx, {
type:'radar',
data:{
labels:['Velocità (MTTR)','Volume risolti','Qualità (FP rate)','SLA compliance','Score totale'],
datasets:[
{ label:'Io', data:[
normalize(r.mttr_avg_h, teamAvgMttr, true),
normalize(r.resolved, teamAvgResolved, false),
normalize(r.fp_rate_pct, teamAvgFp, true),
normalize(100-(r.sla_breach_rate_pct||0), 100-teamAvgSla, false),
Math.round(earned.length/ACHIEVEMENTS.length*100),
], backgroundColor:'rgba(59,130,246,.2)', borderColor:'#3b82f6', pointBackgroundColor:'#3b82f6', pointRadius:3 },
{ label:'Media team', data:[50,50,50,50,50], backgroundColor:'rgba(148,163,184,.1)', borderColor:'rgba(148,163,184,.5)', borderDash:[4,4], pointRadius:0 },
]
},
options:{
responsive:true, maintainAspectRatio:false,
plugins:{ legend:{ position:'bottom', labels:{ font:{size:11}, boxWidth:10 }}},
scales:{ r:{ min:0, max:100, ticks:{ display:false }, grid:{ color:'rgba(128,128,128,.15)' }, pointLabels:{ font:{size:10}, color:'var(--text-2)' }}}
}
});
}
} catch(e) { console.error('loadMyPerformance', e); }
}
// ══════════════════════════════════════════════════════════
// PWA — Operator portal install prompt
// ══════════════════════════════════════════════════════════
(function registerOpsPWA() {
if (!document.querySelector('meta[name="theme-color"]')) {
const meta = document.createElement('meta');
meta.name = 'theme-color'; meta.content = '#1a1a2e';
document.head.appendChild(meta);
}
if (!document.querySelector('link[rel="manifest"]')) {
const link = document.createElement('link');
link.rel = 'manifest';
link.href = 'data:application/json,' + encodeURIComponent(JSON.stringify({
name: (ITSMOPS_CONFIG.project_name || 'NH') + ' IT Ops',
short_name: 'IT Ops',
start_url: './',
display: 'standalone',
background_color: '#1a1a2e',
theme_color: '#156082',
icons: [
{ src: 'https://via.placeholder.com/192x192/156082/ffffff?text=OPS', sizes: '192x192', type: 'image/png' },
{ src: 'https://via.placeholder.com/512x512/156082/ffffff?text=OPS', sizes: '512x512', type: 'image/png' },
]
}));
document.head.appendChild(link);
}
})();